AddCSLuaFile("nwvars.lua")
AddCSLuaFile("packet.lua")

include("packet.lua")

NWTYPE_STRING	= 0
NWTYPE_NUMBER	= 1
NWTYPE_CHAR	= 2
NWTYPE_SHORT	= 3
NWTYPE_FLOAT	= 4
NWTYPE_BOOLEAN	= 5
NWTYPE_VECTOR	= 6
NWTYPE_ANGLE	= 7
NWTYPE_ENTITY	= 8	// this won't be reliable until I hook it up to EntityTransmit so it knows the proper time to tell the client
			// it is ok to use players though

REPL_EVERYONE	= 0
REPL_PLAYERONLY	= 1
REPL_TEAMONLY	= 2

NWSIZES = {}
NWSIZES[NWTYPE_STRING]	= nil
NWSIZES[NWTYPE_NUMBER]	= 4
NWSIZES[NWTYPE_CHAR]	= 1
NWSIZES[NWTYPE_SHORT]	= 2
NWSIZES[NWTYPE_FLOAT]	= 4
NWSIZES[NWTYPE_BOOLEAN] = 1
NWSIZES[NWTYPE_VECTOR]	= 8
NWSIZES[NWTYPE_ANGLE]	= 8
NWSIZES[NWTYPE_ENTITY]	= 4

NWDEBUG = false

// we register a table of networked variables, that we'll set on the entity

// nwtable is a table
// { {name, default, type, proxyfunc} }

local NetworkedEntities = {}

if SERVER then

	require("transmittools")

	local NWStack = {}

	local EntityDirtyTable = {}
	local FullUpdateTable = {}

	function EntityTransmit(plyi, enti)
		local ply, ent = Entity(plyi), Entity(enti)

		if ent._t then
			ent._TransmitPlayers[ply] = true
		end
	end

	function UnMarkEntity(e)
		if NWDEBUG then
			print("Unmarking", e)
		end

		EntityDirtyTable[e] = nil

		for k,v in pairs(NetworkedEntities) do
			if v == e then
				NetworkedEntities[k] = nil
			else
				if v.SeenEntities && v.SeenEntities[e] then
					v.SeenEntities[e] = nil
				end
				if v._TransmitPlayers && v._TransmitPlayers[e] then
					v._TransmitPlayers[e] = nil
				end
			end
		end
	end

	function MarkEntityAsDirty(t, k, v)
		if NWDEBUG then
			print("Dirty", t, k, v)
		end

		local index = t._NWTable[k][1]

		if !index then return end

		local ent = t.Entity or t.Weapon

		EntityDirtyTable[ent] = true
		t._NWDirtyTable[k] = v
	end

	function InvalidatePlayerTeam(ply)
		if !ply.SeenEntities then return end

		local players = team.GetPlayers(ply:Team())

		table.insert(FullUpdateTable, ply)

		for k,v in ipairs(players) do
			if v != ply then
				ply.SeenEntities[v] = nil
			end
		end
	end

	local function BitForPlayer(ply)
		if !ply.bit then
			ply.bit = math.pow(2, ply:EntIndex()-1)
		end

		return ply.bit
	end

	local function BitForTeam(team)
		local players = {}
		local mask = 0

		for k,v in ipairs(player.GetAll()) do
			if v:Team() == team then
				mask = mask + BitForPlayer(v)
				players[v] = true
			end
		end

		return players, mask
	end

	local function MaskedPacket(mask, packets, tbl, recipients)
		local rp = RecipientFilter()
		if type(recipients) == "table" then
			for ply, _ in pairs(recipients) do
				rp:AddPlayer(ply)
			end
		else
			rp:AddPlayer(recipients)
		end

		p = NewPacket(rp)
		table.insert(tbl, p)
		packets[mask] = p

		return p
	end

	function CreateEntityUpdate(tbl)
		local activepackets = {}

		for ent, _ in pairs(EntityDirtyTable) do
			local playermask = 0

			for ply, _ in pairs(ent._TransmitPlayers) do
				if IsValid(ply) then
					playermask = playermask + BitForPlayer(ply)
				else
					if NWDEBUG then
						print("Unexpected _TransmitPlayer")
					end

					ent._TransmitPlayers[ply] = nil
				end
			end

			if playermask > 0 then
				for k, v in pairs(ent._NWDirtyTable) do
					local p = nil

					local t = ent._NWTable[k]
					local i, type, repl = t[1], t[2], t[3]

					if repl == REPL_PLAYERONLY then
						if ent:IsPlayer() then
							local rplymask = BitForPlayer(ent)
							p = activepackets[rplymask] or MaskedPacket(rplymask, activepackets, tbl, ent)
						elseif IsValid(ent:GetOwner()) && ent:GetOwner():IsPlayer() then
							local rply = ent:GetOwner()
							local rplymask = BitForPlayer(rply)
							p = activepackets[rplymask] or MaskedPacket(rplymask, activepackets, tbl, rply)
						end
					elseif repl == REPL_TEAMONLY then
						if ent:IsPlayer() then
							local team, mask = BitForTeam(ent:Team())
							p = activepackets[mask] or MaskedPacket(mask, activepackets, tbl, team)
						end
					else
						p = activepackets[playermask] or MaskedPacket(playermask, activepackets, tbl, ent._TransmitPlayers)
					end

					p:PackEntKeyValue(ent, i, type, v)

					ent._NWDirtyTable[k] = nil
				end
			end

			EntityDirtyTable[ent] = nil
		end
	end

	function CreateFullUpdate(ply, tbl)
		local p = nil

		for k, ent in pairs(NetworkedEntities) do
			if (IsValid(ent) || ent == GetWorldEntity()) && ent._t && !ply.SeenEntities[ent] && ent._TransmitPlayers[ply] then
				if !p then
					if NWDEBUG then
						print("Creating full update for", ply)
					end
					p = NewPacket(ply)
				end

				ply.SeenEntities[ent] = true

				for name, tbl in pairs(ent._NWTable) do
					local i, type, repl = tbl[1], tbl[2], tbl[3]

					if NWDEBUG then
						print("Full update packing", ent, i, type, repl, ent[name])
					end
					
					if (repl == nil || repl == REPL_EVERYONE) || (repl == REPL_TEAMONLY && (ent:IsPlayer() && ent:Team() == ply:Team())) || (repl == REPL_PLAYERONLY && (ent == ply || ent:GetOwner() == ply)) then
						p:PackEntKeyValue(ent, i, type, ent[name])
					elseif NWDEBUG then
						print("not sending ", ent, i, type, ent[name], "[to]", ply)
					end
				end
			elseif (!IsValid(ent) && ent != GetWorldEntity()) then
				if NWDEBUG then
					print("Unexpected missing entity for full update", ent)
				end

				NetworkedEntities[k] = nil
			end
		end

		if p then
			table.insert(tbl, p)
		end
	end

	hook.Add("Tick", "UpdateNWVars", function()
		if #FullUpdateTable > 0 then
			for i,ply in pairs(FullUpdateTable) do
				CreateFullUpdate(ply, NWStack)

				FullUpdateTable[i] = nil
			end
		end

		if table.Count(EntityDirtyTable) > 0 then
			CreateEntityUpdate(NWStack)
		end

		local bytesleft = 64
		for i, p in pairs(NWStack) do
			local sent = p:Dispatch(bytesleft)
			bytesleft = bytesleft - sent

			if p:Size() == 0 then
				if NWDEBUG then
					print("packet completed bytes:", sent, bytesleft)
				end
				NWStack[i] = nil
			end
		end
	end)

	function NWVarPlayerConnect(ply)

		if !table.HasValue(FullUpdateTable, ply) then
			table.insert(FullUpdateTable, ply)
		end

		if !ply.SeenEntities then
			ply.SeenEntities = {}
		end

	end

	concommand.Add("NwVar_SendFullUpdate", NWVarPlayerConnect)

	hook.Add("PlayerDisconnected", "NWCleanupPlayer", function(ply)
		UnMarkEntity(ply)
	end)

	hook.Add("EntityRemoved", "NWCleanup", function(ent)
		if !ent._t || ent:IsPlayer() then return end
		UnMarkEntity(ent)
	end)

	local PlayerMeta = FindMetaTable("Player")
	local setteam = PlayerMeta.SetTeam
	function PlayerMeta:SetTeam(x) 
		setteam(self, x)
		InvalidatePlayerTeam(self)
	end

	local e = Entity
	function Entity(x)
		if x == 0 then return GetWorldEntity() end
		return e(x)
	end

else
	NeedsUpdate = false

	function GetWorldEntity() return Entity(0) end

	usermessage.Hook("N", function(um)
		local data = ParseFromUm(um)
	end)

	hook.Add("Tick", "UpdateMe", function()
		if NeedsUpdate && SafeToSend then
			RunConsoleCommand("NwVar_SendFullUpdate")
			NeedsUpdate = false
		end
	end)
end


function RegisterNWTable(entity, nwtable)
	if type(nwtable) != "table" then Error("Invalid NW table") return end
	if entity._t then Error("Already setup") return end

	local nlookup = {}

	for i=1, #nwtable do
		local tbl = nwtable[i]

		if type(tbl) != "table" then Error("Invalid NW table") return end

		entity[tbl[1]] = tbl[2]

		nlookup[tbl[1]] = {i, tbl[3], tbl[5]}

		if CLIENT then
			nlookup[i] = {tbl[1], tbl[3], tbl[4]}
		end
	end

	local _t = entity:GetTable()

	local etable = { _t = _t, _NWTable = nlookup, _NWDirtyTable = {}, _TransmitPlayers = {} }
    
	local mt = {
		__index = function (t,k)
			return t._t[k]
		end,

		__newindex = function (t,k,v)
			if SERVER && t._NWTable[k] then
				if t._t[k] != v then
					MarkEntityAsDirty(t,k,v)
				end
			end

			t._t[k] = v
		end
	}

	setmetatable(etable, mt)

	entity:SetTable(etable)

	if CLIENT then
		NeedsUpdate = true
	else
		table.insert(NetworkedEntities, entity)
	end
end

local GlobalTable = {}
local PlayerTable = {}

hook.Add("InitPostEntity", "SetupNWTableGlobal", function()
	RegisterNWTable(GetWorldEntity(), GlobalTable)
end)

hook.Add("OnEntityCreated", "SetupNWTablePly", function(ply)
	if IsValid(ply) && ply:GetClass() == "player" && !ply._t then
		RegisterNWTable(ply, PlayerTable)
	end
end)

function MergeTablesI(a, b)
	for k,v in ipairs(b) do
		table.insert(a, v)
	end
end

function RegisterNWTableGlobal(nwtable)
	MergeTablesI(GlobalTable, nwtable)
end

function RegisterNWTablePlayer(nwtable)
	MergeTablesI(PlayerTable, nwtable)
end
